package sweet.mvc.core

import javax.servlet.{ ServletContext }
import javax.servlet.http.{ HttpServletRequest => JReq, HttpServletResponse => JResp, _ }
import java.io.Writer
import freemarker.template.{ Configuration, TemplateModel, ObjectWrapper }
import freemarker.ext.servlet.{ FreemarkerServlet, AllHttpScopesHashModel }
import freemarker.ext.beans.{ BeansWrapper }
import sweet.helper.{ Logger, JSystem, StringHelper }
import sweet.mvc.api._

/** Process a request and and response */
trait RequestProcessor{
  def process(req: Req, resp: Resp): Unit
}

/** A parser the digest the request uri that needed for Sweet application to route process. */
trait ReqRouteParser {
  def parse(req: Req): ReqRoute
}
  
/** Parse URI that match the following format: 
 * <code>/contextName/dispatcherName/controllerName/handlerName</code>
 * <code>/contextName/dispatcherName/controllerName /defaultHandlerName</code>
 * <code>/contextName/dispatcherName /defaultControllerName/defaultHandlerName</code>
 * <code>/dispatcherName/controllerName/handlerName</code>
 * 
 * This URI is resulted in mapping the dispatcher servlet with <code>/dispaatcherName/&#42;</code> (asterisk) in web.xml.
 */
class InPathDispatcherNameParser(
  val defaultControllerName: String, 
  val defaultHandlerName: String) extends ReqRouteParser with Logger {
    
  import StringHelper._
  
  def parse(req: Req): ReqRoute = {
    val uri = req.uri
    logger.debug("Parsing uri "+uri)
    val Array(contextName, dispatcherName, rest@_*) = uri.fastSplit("/")
    
    var controllerName = defaultControllerName
    var handlerName = defaultHandlerName
    var query = ""
    if(rest.length>0 && rest(0) != ""){
      val rest1 = rest.toList
      controllerName = "/"+rest1.head
      val rest2 = rest1.tail
      if(rest2.length>0 && rest2(0) != ""){
        handlerName = "/"+rest2.head
        val rest3 = rest2.tail
        if(rest3.length>0 && rest3(0) != ""){
          query = rest3.mkString("/","/","")
        }
      }
    }
    val reqRoute = new ReqRoute("/"+contextName, "/"+dispatcherName, controllerName, handlerName, query)
    logger.debug("reqRoute "+ reqRoute)
    reqRoute
  }
}

/** Parse URI that match the following: 
 * <code>contextName/controllerName/handlerName.MYEXTENSION</code>
 *
 * This URI is resulted in mapping the dispatcher servlet with "*.MYEXTENSION" in web.xml.
 */
class ExtensionDispatcherNameParser extends ReqRouteParser {
  def parse(req: Req): ReqRoute = {
    throw new Exception("not yet implemented.")
  }
}

/** ViewRender perform the output to response object using the req as models. 
 *
 * View is usuage reanderd from a template file using the given name. */
trait ViewRender{
  val viewPrefix: String
  val viewSuffix: String
  def contentType: String
  def render(name: String, reqRoute: ReqRoute, req: Req, resp: Resp): Unit
}

/** Render output using the Freemarker template engine.
 * 
 * We are extending FreemarkerServlet to reuse some of it's provided methods. Since members are private
 * this class, we can only use method that are exposing parameters where we can pass our own. Currently 
 * we are overriding/using deduceLocale and createModel methods.
 *
 * The render method is shorter version of original FreemarkerServlet's processRequest method.
 * 
 */
class FreemarkerViewRender(
  val contentType: String,
  val viewPrefix: String,
  val viewSuffix: String,
  val servletContext: ServletContext,
  val freemarkerConfig: Configuration) 
  extends FreemarkerServlet with ViewRender with Logger {
  
  protected lazy val scalaWrapper = freemarkerConfig.getObjectWrapper
  
  //original FMServlet constant, they were private, so have to redefine them here.
  val KEY_APPLICATION = "Application"
  val KEY_SESSION = "Session"
  val KEY_REQUEST = "Request"
  
  val LOWCASE_KEY_APPLICATION = "app"
  val LOWCASE_KEY_SESSION = "sess"
  val LOWCASE_KEY_REQUEST = "req"
  
  val prefixViewNameWithControllerName = true
    
  /** Default content type is text/html */
  def this(prefix: String, suffix: String, ctx: ServletContext, cfg: Configuration){
    this("text/html", prefix, suffix, ctx, cfg)
  }
  
  /** Implementation of rendering a view - delegate to Freemarker template process */
  def render(name: String, reqRoute: ReqRoute, req: Req, resp: Resp): Unit = {
    logger.debug("Rendering reqRoute " + reqRoute)
    val servletReq = req.servletReq
    val servletResp = resp.servletResp
    
    //viewName = prefix+dynamic+sufix
    val viewName = 
      viewPrefix + 
      (if(prefixViewNameWithControllerName) reqRoute.controllerName+name else name) + 
      viewSuffix
      
    logger.debug("Get template name " + viewName)
    val template = freemarkerConfig.getTemplate(viewName, deduceLocale(name, servletReq, servletResp))
    
    val customContentType = template.getCustomAttribute("content_type")
    val ctype = if (customContentType != null) customContentType.asInstanceOf[String] else contentType
    logger.debug("Setting response content type " + ctype)
    resp.setContentType(ctype)
        
    logger.debug("Preparing models for templates")
    val model = createModel(scalaWrapper, servletContext, servletReq, servletResp)
    
    logger.debug("Rendering template.")
    template.process(model, resp.writer)
  }  
  
  /** Override to simulate a servlet instance so createModel can create objects properly. */
  override def getServletContext = servletContext 
  
  /** Override to return local from our own config obj. */
  override def deduceLocale(name: String, req: JReq, resp: JResp) = freemarkerConfig.getLocale
  
  /** Generate model that searchable in Request, Session, Application scope. */
  override def createModel(scalaWrapper: ObjectWrapper, servletContext: ServletContext, 
    req: JReq, resp: JResp): TemplateModel = {
      
    val ret = super.createModel(scalaWrapper, servletContext, req, resp)
    val model: AllHttpScopesHashModel = ret.asInstanceOf[AllHttpScopesHashModel]
    
    // Duplicate existing FM models into lowercase key names. */
    model.putUnlistedModel(LOWCASE_KEY_APPLICATION, model.get(KEY_APPLICATION))
    model.putUnlistedModel(LOWCASE_KEY_SESSION, model.get(KEY_SESSION))
    model.putUnlistedModel(LOWCASE_KEY_REQUEST, model.get(KEY_REQUEST))
        
    model //return model
  }
}

/**
 * Provides a default request processing workflow as follows:
 * 
 *    1. parse incoming URL according to a reqRouteParser rule
 *    2. find a controller that mapped to incoming URL
 *    3. find a handler from the above controller that match to incoming URL
 *    4. invoke the handler and receive a view object back.
 *    5. export any models in view object into request scope
 *    6. invoke viewRender.render(view, req) that produce output to response                  
 *       
 */
class SweetRequestProcessor(
  val reqRouteParser: ReqRouteParser,
  val interceptors: scala.collection.Map[String, List[Interceptor]],
  val controllers: scala.collection.Map[String, Controller],
  val viewEngine: ViewRender)
  extends RequestProcessor with Logger {
  
  val REDIRECT_PREFIX = "redirect:"
  val FORWARD_PREFIX = "forward:"
  
  import Constants._
  
  def process(req: Req, resp: Resp): Unit = {
    val reqRoute = reqRouteParser.parse(req)
    val servletReq = req.servletReq    
    
    //Internal method just for the processView
    import sweet.helper.StringHelper._
    def processView(view: View){
      val viewName = view.name
      if(viewName.startsWith(REDIRECT_PREFIX)){
        val vName = viewName.substring(REDIRECT_PREFIX.length)
        val names = vName.fastSplit("/")
        val redirectName = if(names.length ==1) req.reqRoute.controllerName + vName else vName
        logger.info("Redirecting view "+redirectName)
        resp.servletResp.sendRedirect(req.reqRoute.dispatcherPath+redirectName)
      }else if(viewName.startsWith(FORWARD_PREFIX)){
        val vName = viewName.substring(FORWARD_PREFIX.length)
        val names = vName.fastSplit("/")
        val forwardName = if(names.length ==1) req.reqRoute.controllerName + vName else vName
        logger.info("Forwarding view "+forwardName)
        servletReq.getRequestDispatcher(req.reqRoute.dispatcherName+forwardName).forward(servletReq, resp.servletResp)
      }else{
        //add models to request scope and return view
        for((k,v) <- view.models){
          logger.debug("Adding view model "+k+" to request scope.")
          servletReq.setAttribute(k, v)
        }
        
        logger.info("Rendering view "+viewName)
        viewEngine.render(viewName, req.reqRoute, req, resp)
      }
    }
    
    //Saving sweet core variables to request object in case Controller needs to query them.
    servletReq.setAttribute(REQ_ROUTE_KEY, reqRoute)
    servletReq.setAttribute(CONTROLLER_HANDLERS_KEY, controllers)
    servletReq.setAttribute(CONTROLLER_INTERCEPTORS_KEY, interceptors)
                         
    logger.info("Request received " + reqRoute)
    val controllerName = reqRoute.controllerName
    controllers.get(controllerName) match {
      case Some(controller) => {
        val t1 = JSystem.now  
        
        logger.debug("Processing with controller " + reqRoute.controllerName)
        controller.findHandler(reqRoute) match {
          case Some(handler) => {
            val handlerName = reqRoute.handlerName
            logger.debug("Processing with handler " + handlerName)
            
            val interceptorsOption = interceptors.get(controllerName+handlerName) 
            var isViewDone = false
            
            // == Invoke BEFORE handler interceptor if there is any.
            interceptorsOption match {
              case Some(itcs) => {
                for(itc <- itcs){
                  logger.debug("Invoking before handler interceptor " + itc)
                  itc.beforeReq(req) match {
                    case Some(view) => { // do view now, and skip controller.
                      logger.debug("Interceptor returned a view. Stop further handler processing.")
                      processView(view) 
                      isViewDone = true 
                    }
                    case None => // no custom view... let controller handle request
                  }             
                }
              }
              case None => // no interceptor found
            }
            
            if(!isViewDone){
              handler(req, resp) match {
                case view: EmptyView => {
                  logger.debug("Handler wants a empty view. Will do no further output.")
                  
                }
                case view: HandlerNameView => {
                  val updatedRR = req.reqRoute
                  logger.debug("Handler wants a dynamic handlerName view " + updatedRR.handlerName)
                  processView(new View(updatedRR.handlerName, view.models))
                }
                case view@ _ => {
                  logger.debug("Handler wants a specific template view " + view.name)
                  processView(view)
                }
              }
              
              // == Invoke AFTER handler interceptor if there is any.
              interceptorsOption match {
                case Some(itcs) => 
                  for(itc <- itcs){
                    logger.debug("Invoking after handler interceptor " + itc)        
                    itc.afterReq(req)
                  }
                case None => // no interceptor found
              }
            }
          }
          case None => throw new Exception("Handler not found from " + reqRoute)
        }// end of handler processing
                     
        //log processing time
        val t2 = JSystem.now
        logger.info("Request processed in " +(t2 - t1)+" millis seconds.")
              
      }// end of controller found
      case None => throw new Exception("Controller not found from " + reqRoute)
    }
  }
}
